// main.cpp

#include "Subdiv.h"
#include "VBO.h"
#include "Raster.h"
#include "gl/glew.h"
#include "gl/glut.h"

////////////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////CG Part///////////////////////////////////////////
//#include <nvGlutManipulators.h>
//#include <nvModel.h>
//#include <nvGlutWidgets.h>
//#include <nvSDKPath.h>

#include <Cg/cg.h>
#include <Cg/cgGL.h>

#pragma comment( lib, "glew32.lib" ) 
#pragma comment( lib, "glew32s.lib" ) 
#pragma comment( lib, "cg.lib" ) 
#pragma comment( lib, "cgGL.lib" ) 

#define SHADER_PATH "shaders/"
CGcontext cg_context; 
CGprofile cg_vprofile, cg_gprofile, cg_fprofile;
GLuint rand_tex, noise_tex;

struct Program {
	const char *name;
	const char *filename;
	const char *vprog_entry, *gprog_entry, *fprog_entry;    // entry points for vertex, geometry and fragment programs
	GLenum prim;
	bool multipass;
	CGprogram vprog, gprog, fprog;
};

Program prog[] = {
	//{ "rasterize_bbox", SHADER_PATH "rasterize.cg", "rasterize_vp", "rasterize_gp", 0, GL_TRIANGLES, false, 0, 0, 0 },
	//{ "rasterize", SHADER_PATH "rasterize.cg", "rasterize_vp", "rasterize_gp", "rasterize_fp", GL_TRIANGLES, false, 0, 0, 0 },
	{ "passthru", SHADER_PATH "passthru.cg", "transform_light_vp", "passthru_gp", 0, GL_TRIANGLES, false, 0, 0, 0 },
	//{ "shrink", SHADER_PATH "passthru.cg", "transform_light_vp", "shrink_gp", 0, GL_TRIANGLES, false, 0, 0, 0 },
	//{ "silhouette", SHADER_PATH "silhouette.cg", "silhouette_vp", "silhouette_gp", 0, GL_TRIANGLES_ADJACENCY_EXT,  false, 0, 0, 0 },
};

#define NPROGS sizeof(prog) / sizeof(Program)
#define CHECK_ERRORS  \
{ \
	GLenum err = glGetError(); \
	if (err) \
	printf( "Error %x at line %d\n", err, __LINE__); \
}

Program *current_prog = &prog[0];
int current_prog_idx = 0;

// Callback function used for reporting Cg compile errors
void cgErrorCallback()
{
	CGerror lastError = cgGetError();
	if(lastError)
	{
		printf("%s\n", cgGetErrorString(lastError));
		printf("%s\n", cgGetLastListing(cg_context));
		exit(1);
	}
}

float frand()
{
	return rand() / (float) RAND_MAX;
}

GLuint create_rand_texture(GLenum target, GLenum format, int w, int h)
{
	float *data = new float [w*h*4];
	float *ptr = data;
	for(int i=0; i<w*h; i++) {
		*ptr++ = frand();
		*ptr++ = frand();
		*ptr++ = frand();
		*ptr++ = frand();
	}

	GLuint texid;
	glGenTextures(1, &texid);
	glBindTexture(target, texid);

	glTexParameteri(target, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
	glTexParameteri(target, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
	glTexParameteri(target, GL_TEXTURE_WRAP_S, GL_REPEAT);
	glTexParameteri(target, GL_TEXTURE_WRAP_T, GL_REPEAT);

	glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
	glTexImage2D(target, 0, format, w, h, 0, GL_RGBA, GL_FLOAT, data);

	return texid;
}

GLuint create_noise_texture(GLenum format, int w, int h, int d)
{
	float *data = new float [w*h*d*4];
	float *ptr = data;
	for(int i=0; i<w*h*d; i++) {
		*ptr++ = frand()*2.0-1.0;
		*ptr++ = frand()*2.0-1.0;
		*ptr++ = frand()*2.0-1.0;
		*ptr++ = frand()*2.0-1.0;
	}

	GLuint texid;
	glGenTextures(1, &texid);
	GLenum target = GL_TEXTURE_3D;
	glBindTexture(target, texid);

	glTexParameteri(target, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	glTexParameteri(target, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
	glTexParameteri(target, GL_TEXTURE_WRAP_S, GL_REPEAT);
	glTexParameteri(target, GL_TEXTURE_WRAP_T, GL_REPEAT);
	glTexParameteri(target, GL_TEXTURE_WRAP_R, GL_REPEAT);

	glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
	glTexImage3D(target, 0, format, w, h, d, 0, GL_RGBA, GL_FLOAT, data);

	return texid;
}

//////////////////////////////////////////////////////////////////////////////////////////////////

void SetupLights();
void AutoView();

// Target frame rate and timer.
static const int kFramesPerSecond = 30;
static STTimer gClockTimer;

// Window size so we can compute aspect ratio.
static int gWindowSizeX = 300;
static int gWindowSizeY = 300;

// Your subdivision surface
static CatClarkSurface* gSubdiv = 0;
static VBO* gVbo = 0;
static Raster* gRaster = 0;

GLdouble proM[16];
GLdouble modelM[16];
GLint view[4];

// Current wireframe setting.
bool gWireframe = false;

// Input and Output settings
char* gInputFile = 0;
char* gOutputFile = 0;
int gOutputIterations = 0;

// camera control
const double PI = 3.1415926;
float gMotionFactor = 0.1;
float gAngleXZ = 0;
float gAngleY = 0;
float gDist = 0;
int gPreviousMouseX = -1;
int gPreviousMouseY = -1;
int gMouseButton = -1;
STVector3 gBBoxMin, gBBoxMax;
STVector3 gCameraEye(0, 0, 1);
STVector3 gCameraLookAt(0, 0, 0);
STVector3 gCameraUp(0, 1, 0);
float gNear = 0.1f;
float gFar  = 100.f;

int gNSub = 0;
int gCamPos = 0;


void Setup()
{
  AutoView();
  SetupLights();
  gClockTimer.Reset();
}

void SetupLights()
{
  // White directional light (1, 1, 1)
  GLfloat light0_position[] = { 1, 1, 1, 0 };
  GLfloat light0_diffuse[] = { 0.8, 0.8, 0.8, 1 };
  GLfloat light0_specular[] = { 1, 1, 1, 1 };
  glLightfv( GL_LIGHT0, GL_POSITION, light0_position );
  glLightfv( GL_LIGHT0, GL_DIFFUSE, light0_diffuse );
  glLightfv( GL_LIGHT0, GL_SPECULAR, light0_specular );

  // light 1
  GLfloat light1_position[] = { -1, 1, 1, 0 };
  GLfloat light1_diffuse[] = { 0.3, 0.3, 0.3, 1 };
  GLfloat light1_specular[] = { 1, 1, 1, 1 };
  glLightfv( GL_LIGHT1, GL_POSITION, light1_position );
  glLightfv( GL_LIGHT1, GL_DIFFUSE, light1_diffuse );
  glLightfv( GL_LIGHT1, GL_SPECULAR, light1_specular );

  // material

	glEnable(GL_COLOR_MATERIAL);
	glColorMaterial(GL_FRONT, GL_DIFFUSE);
	glShadeModel(GL_FLAT);

  /*GLfloat mat_ambient[] = { 0, 0, 0, 1 };
  GLfloat mat_specular[] = { .2f, .2f, .2f, 1 };
  GLfloat mat_diffuse[] = { .6f, .1f, .1f, 1 };
  GLfloat mat_shininess[] = { 128 };
  glMaterialfv( GL_FRONT, GL_AMBIENT, mat_ambient );
  glMaterialfv( GL_FRONT, GL_SPECULAR, mat_specular );
  glMaterialfv( GL_FRONT, GL_DIFFUSE, mat_diffuse );
  glMaterialfv( GL_FRONT, GL_SHININESS, mat_shininess );

  GLfloat bmat_ambient[] = { 0, 0, 0, 1 };
  GLfloat bmat_specular[] = { 0, 0, 0, 1};
  GLfloat bmat_diffuse[] = { 0.1f, .1f, .8f, 1 };
  GLfloat bmat_shininess[] = { 128 };
  glMaterialfv( GL_BACK, GL_AMBIENT, bmat_ambient );
  glMaterialfv( GL_BACK, GL_SPECULAR, bmat_specular );
  glMaterialfv( GL_BACK, GL_DIFFUSE, bmat_diffuse );
  glMaterialfv( GL_BACK, GL_SHININESS, bmat_shininess );

  GLfloat lmodel_ambient[] = { 0, 0, 0, 1 };
  glLightModelfv(GL_LIGHT_MODEL_AMBIENT, lmodel_ambient);
  glLightModeli( GL_LIGHT_MODEL_TWO_SIDE, 1 );*/

  glEnable(GL_NORMALIZE);
  glEnable(GL_LIGHTING);
  glEnable(GL_LIGHT0);
  glEnable(GL_LIGHT1);
  //glEnable(GL_MULTISAMPLE);

	// set normal rendering options
	glEnable(GL_DEPTH_TEST);
	glBlendFunc(GL_SRC_ALPHA, GL_ONE);
	//glEnable(GL_BLEND);
	//glDepthFunc(GL_LEQUAL);
	glDisable(GL_CULL_FACE);
}

void DisplayCallback()
{
		// background color 
    glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glMatrixMode( GL_PROJECTION );
    glLoadIdentity();

    float aspectRatio = float(gWindowSizeX) / float(gWindowSizeY);
    gluPerspective(60.0f, aspectRatio, gNear, gFar);

    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
    gluLookAt(gCameraEye.x, gCameraEye.y, gCameraEye.z,
              gCameraLookAt.x, gCameraLookAt.y, gCameraLookAt.z,
              gCameraUp.x, gCameraUp.y, gCameraUp.z);

		/*cgGLBindProgram(current_prog->vprog);
		cgGLEnableProfile(cg_vprofile);

		cgGLBindProgram(current_prog->gprog);
		cgGLEnableProfile(cg_gprofile);

		if (current_prog->fprog) {
			cgGLBindProgram(current_prog->fprog);
			cgGLEnableProfile(cg_fprofile);
		}*/

    
    // Draw your surface.
    
    // gVbo->draw();
		
		glColor3f(0.7, 0.2, 0.3);
		gSubdiv->Draw();

		glDisable(GL_LIGHTING);
		glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
		glEnable(GL_POLYGON_OFFSET_LINE);
		glPolygonOffset(-0.5f, -1.0f);
	
		glColor3f(0.2, 0.2, 0.2);
		gSubdiv->Draw();

		glDisable(GL_POLYGON_OFFSET_LINE);
		glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
		glEnable(GL_LIGHTING);

	/*	glActiveTexture(GL_TEXTURE0);
		glBindTexture(GL_TEXTURE_RECTANGLE_NV, rand_tex);

		glActiveTexture(GL_TEXTURE1);
		glBindTexture(GL_TEXTURE_3D, noise_tex);

		cgGLDisableProfile(cg_vprofile);
		cgGLDisableProfile(cg_gprofile);
		cgGLDisableProfile(cg_fprofile);*/

		//CHECK_ERRORS;

    glutSwapBuffers();
}

void AutoView() {

  gSubdiv->GetBounds(gBBoxMin, gBBoxMax);

  // tan(fov/2) = (.5 * box_size)/dist
  // dist = (.5 * box_size) / tan(fov/2)
  STVector3 mid_point = .5f * (gBBoxMin + gBBoxMax);
  float height = gBBoxMax.y - gBBoxMin.y;
  float width  = gBBoxMax.x - gBBoxMin.x;
  float max_size = std::max(width, height);
  const float kTan30 = 0.577350269f;
  gDist = .5 * (max_size) / kTan30 + gBBoxMax.z;
  gDist *= 1.1; // a little fudge factor to avoid filling the screen
               // entirley

  gCameraEye = STVector3(sin(gAngleXZ)*gDist, sin(gAngleY)*gDist, cos(gAngleXZ)*gDist);
  gCameraLookAt = STVector3(0,0,0); //gCameraEye + STVector3(0, 0, -1);
  gCameraUp = STVector3(0, 1, 0);
  gNear = .01f;
  gFar  = (gCameraEye.z - gBBoxMin.z)*10;
}

void MoveCamera(char type)
{
	switch (type)
	{
	case 'w':
		gDist -= gMotionFactor;
		break;
	case 's':
		gDist +=gMotionFactor;
		break;
	case 'a':
		gCameraUp += gMotionFactor;
		if(gAngleXZ >= 2*PI)
			gAngleXZ -= 2*PI;
		break;
	case 'd':
		gAngleXZ -= gMotionFactor;
		if(gAngleXZ < 0)
			gAngleXZ += 2*PI;
		break;
	case 'q':
		gCameraLookAt.x += 0.1f;
		break;
	case 'e':
		gCameraLookAt.x -= 0.1f;
		break;
	}

	gCamPos++;
	// to be update to current nv sdk camera control
	gCameraEye = STVector3(sin(gAngleXZ)*gDist, sin(gAngleY)*gDist, cos(gAngleXZ)*gDist);
	glutPostRedisplay();
}

void Rasterize()
{
	glGetDoublev(GL_MODELVIEW_MATRIX, modelM);
	glGetDoublev(GL_PROJECTION_MATRIX, proM);
	glGetIntegerv(GL_VIEWPORT, view);
	gRaster->init(gSubdiv->GetVertexVBOData(), gSubdiv->GetNormalVBOData(), gSubdiv->GetColorVBOData(), gSubdiv->GetIndexVBOData(), gSubdiv->GetPatchVBOData(), gSubdiv->GetNumVertex(), gSubdiv->GetNumIndex(), gSubdiv->GetNumPathces());
	//gRaster->raster(modelM, view, proM); // mvp 
	gRaster->executionTime(modelM, view, proM);
	glClearColor(1.0, 1.0, 1.0, 1.0);
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
	// glRasterPos2i(50, 250);
	glutSwapBuffers();
  glutPostRedisplay();
}

void KeyboardCallback(unsigned char key, int x, int y)
{
		  switch (key) {
		case 'n':
			// Subdivide your mesh once
			gNSub++;
			glGetDoublev(GL_MODELVIEW_MATRIX, modelM);
			glGetDoublev(GL_PROJECTION_MATRIX, proM);
			glGetIntegerv(GL_VIEWPORT, view);
			gSubdiv->SetMVP(modelM, view, proM);
			gSubdiv->Subdivide(1);
			gVbo->init(gSubdiv->GetVertexVBOData(), gSubdiv->GetNormalVBOData(), gSubdiv->GetColorVBOData(), gSubdiv->GetIndexVBOData(), gSubdiv->GetNumVertex(), gSubdiv->GetNumIndex());
			glutPostRedisplay();
			break;
		case 'x':
			//
			// Dump your mesh to file, and save as gOutputFile or
			// subdivided.obj by default.
			//
			gSubdiv->SaveOBJ((gOutputFile) ? gOutputFile : "subdivided.obj");
			break;
		case 'z':
			//
			// Toggle wireframe
			//
			gWireframe = !gWireframe;
			if(gWireframe) {
				glLineWidth(0.5f);
				glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
			} else {
				glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
			}
			glutPostRedisplay();
			break;
			// control camera up or down
		case 'w':
			MoveCamera('w');
			break;
		case 's':
			MoveCamera('s');
			break;
		case 'a':
			MoveCamera('a');
			break;
		case 'd':
			MoveCamera('d');
			break;
		case 'q':
			MoveCamera('q');
			break;
		case 'e':
			MoveCamera('e');
			break;
		case 'c':
			AutoView();
		case 'r':
		  gRaster->setSubN(gNSub);
			gRaster->setCamPos(gCamPos);
			Rasterize();
			break;
	/*	case 't':
			glutSwapBuffers();
			gRaster->saveGLScreen();
			glutSwapBuffers();
			break;*/
		case '1':
			current_prog_idx++;
			if (current_prog_idx> NPROGS-1)
				current_prog_idx = 0;
			current_prog = &prog[current_prog_idx];
			glutPostRedisplay();
			break;
		case '2':
			current_prog --;
			if (current_prog<0)
				current_prog_idx = NPROGS -1;
			current_prog = &prog[current_prog_idx];
			glutPostRedisplay();
			break;
  }
}

void ReshapeCallback(int w, int h)
{
  gWindowSizeX = w;
  gWindowSizeY = h;
  glViewport(0, 0, gWindowSizeX, gWindowSizeY);
  glutPostRedisplay();
}

void MouseCallback(int button, int state, int x, int y)
{
  if (button == GLUT_LEFT_BUTTON || button == GLUT_RIGHT_BUTTON) {
    gMouseButton = button;
  } else {
    gMouseButton = -1;
  }

  if (state == GLUT_UP) {
    gPreviousMouseX = -1;
    gPreviousMouseY = -1;
  }
}

void MouseMotionCallback(int x, int y)
{
  if (gPreviousMouseX >= 0 && gPreviousMouseY >= 0) {
    //compute delta
    float deltaX = x-gPreviousMouseX;
    float deltaY = y-gPreviousMouseY;

    gPreviousMouseX = x;
    gPreviousMouseY = y;

  } else {
    gPreviousMouseX = x;
    gPreviousMouseY = y;
  }
}

void printUsage() {
  printf("Usage:\n");
  printf("./subdivide some_mesh.obj [num_iterations output_mesh.obj]\n");
}

void init_cg()
{
	// create some helper textures
	rand_tex = create_rand_texture(GL_TEXTURE_RECTANGLE_NV, GL_RGBA16F_ARB, 256, 256);
	noise_tex = create_noise_texture(GL_RGBA16F_ARB, 16, 16, 16);

	// get geometry program limits
	GLint max_output_vertices, max_total_output_components;
	glGetProgramivARB(GL_GEOMETRY_PROGRAM_NV, GL_MAX_PROGRAM_OUTPUT_VERTICES_NV, &max_output_vertices);
	glGetProgramivARB(GL_GEOMETRY_PROGRAM_NV, GL_MAX_PROGRAM_TOTAL_OUTPUT_COMPONENTS_NV, &max_total_output_components);
	printf("max output vertices = %d\n", max_output_vertices);
	printf("max total output components = %d\n", max_total_output_components);

	// Create Cg Context
	cg_context = cgCreateContext();
	cgSetErrorCallback(cgErrorCallback);

	// load Cg programs
	cg_vprofile = cgGLGetLatestProfile(CG_GL_VERTEX);
	cg_gprofile = cgGLGetLatestProfile(CG_GL_GEOMETRY);
	cg_fprofile = cgGLGetLatestProfile(CG_GL_FRAGMENT);

	const char *args[] = {
		"-unroll",
		"all",
		0,
	};

	// load programs
	for(int i=0; i<NPROGS; i++) 
	{
		Program &p = prog[i];
		p.vprog = cgCreateProgramFromFile(cg_context, CG_SOURCE, p.filename, cg_vprofile, p.vprog_entry, 0);
		cgGLLoadProgram(p.vprog);

		p.gprog = cgCreateProgramFromFile(cg_context, CG_SOURCE, p.filename, cg_gprofile, p.gprog_entry, args);
		cgGLLoadProgram(p.gprog);

		if (p.fprog_entry) {
			p.fprog = cgCreateProgramFromFile(cg_context, CG_SOURCE, p.filename, cg_fprofile, p.fprog_entry, 0);
			cgGLLoadProgram(p.fprog);
		}
	}
}

void init_opengl()
{
	// glew setup
	glewInit();
	if (!glewIsSupported(
		"GL_VERSION_2_0 "
		"GL_ARB_vertex_program "
		"GL_ARB_fragment_program "
		"GL_NV_gpu_program4 " // includes GL_NV_geometry_program4
		"GL_NV_texture_rectangle "
		"GL_ARB_texture_float "
		))
	{
		printf("Unable to load necessary extension(s)\n");
		printf("This sample requires:\n  OpenGL 2.0\n  GL_ARB_vertex_program\n  GL_ARB_fragment_program\n"
			"GL_NV_gpu_program4\n  GL_NV_texture_rectangle\n  GL_ARB_texture_float\nExiting...\n");
		exit(-1);
	}

	init_cg();
}

int main(int argc, char** argv)
{
	// glut setup
	// glutInit(&argc, argv);
	glutInitWindowPosition(20, 20);
	glutInitDisplayMode( GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH);
	glutInitWindowSize(gWindowSizeX, gWindowSizeY);
	glutCreateWindow("Subdivision");

	init_opengl();

	// setup subdivision object
  gInputFile = argv[1];
  gSubdiv = new CatClarkSurface(gInputFile);

	gVbo = new VBO();
	gVbo->init(gSubdiv->GetVertexVBOData(), gSubdiv->GetNormalVBOData(), gSubdiv->GetColorVBOData(), gSubdiv->GetIndexVBOData(), gSubdiv->GetNumVertex(), gSubdiv->GetNumIndex());
  
	gRaster = new Raster(gWindowSizeX, gWindowSizeY);
	gRaster->init(gSubdiv->GetVertexVBOData(), gSubdiv->GetNormalVBOData(), gSubdiv->GetColorVBOData(), gSubdiv->GetIndexVBOData(), gSubdiv->GetPatchVBOData(), gSubdiv->GetNumVertex(), gSubdiv->GetNumIndex(), gSubdiv->GetNumPathces());
	gRaster->setName(gInputFile);

	if (argc == 4) {
    gOutputIterations = atoi(argv[2]);
    gOutputFile = argv[3];
    gClockTimer.Reset();
    gSubdiv->Subdivide(gOutputIterations);
    float ms = gClockTimer.GetElapsedMillis();
    printf("Subdividing %s %d times took %5.3f seconds\n", gInputFile, gOutputIterations, ms / 1000.f);
    gSubdiv->SaveOBJ(gOutputFile);
    return 0;
  }
	
	// setup callbacks
	Setup();
	glutDisplayFunc(DisplayCallback);
	glutKeyboardFunc(KeyboardCallback);
	glutReshapeFunc(ReshapeCallback);
	glutMouseFunc(MouseCallback);
	glutMotionFunc(MouseMotionCallback);
  glutMainLoop();

  // Call any cleanup code here.
  return 0;
}
